class Patch(object):
    """
    Holds a serializable (i.e. stringifiable) representation of a unified 
    diff.  It is also capable of reconstructing the edit from the original.
    Uses difflib to construct the diff.
    
    Usage (doctest):
    
    >>> p = Patch(orig, edit)
    >>> Patch(edit, p.apply(orig)).diff #look, they're the same!
    []
    
    #>>> #str(p) # doctest:+ELLIPSIS
    
    #"['@@ -26,24 +26,16 @@\\n', ' give', ' you', ' a', '-complete', '-account', '-of', '-the', '-system,', '-\\n\\nand', '-expound', '-the', ' actual', ' teachings', ' of', '-the', '+teh', ' great', ' explorer', ' of', '-the', '+teh', ' truth,', '-\\n\\nthe', '+\\n\\nteh', ' master-builder', ' of', ' human', '@@ -81,7 +73,7 @@\\n', ' Nor', ' again', ' is', '-there', '+tehre', ' anyone', ' who', ' loves']"
    
    >>> Patch(str(p)) == p #supports casting from the stringified diff
    True
    >>> result = Patch(str(p)).apply(orig) #== edit
    >>> Patch(edit, result).diff
    []
    >>> p2 = Patch(edit, edit2)
    >>> result = (p + p2).apply(orig)
    >>> Patch(edit2, result).diff #test adding.  should be the same.
    []
    >>> result = p.apply_rev(edit)
    >>> Patch(orig, result).diff #test apply_rev
    []
    >>> result = (-p).apply(edit)
    >>> Patch(orig, result).diff #test negative
    []
    >>> (p - p).diff #test subtraction # = p + (-p)
    []
    
    
    """
    def __init__(self, *args, **kwargs):
        if 'hunks' in kwargs:
            #for passing in lists of hunks.
            self.diff = kwargs['hunks']
            return
            
        if len(args) == 2 and isinstance(args[0],str) and isinstance(args[1],str):
            #passing strings to diff
            import difflib
            self._orig = list(difflib.unified_diff(
                args[0].split(' '), args[1].split(' ')
            ))[2:]
        elif len(args) == 1 and isinstance(args[0], list):
            self._orig = args[0]
        elif len(args) == 1 and isinstance(args[0], str):
            import simplejson
            self._orig = simplejson.loads(args[0])
        else:
            raise TypeError('Parse doesn\'t know what you\'re talking about.')
        
        self.diff = self._to_diff(self._orig)

        
    #@classmethod
    #def load(cls, serialized):
    #    import simplejson
    #    return cls(simplejson.loads(serialized))
        
    @staticmethod
    def _to_diff(loaded):
        diff = []
        current = []
        for line in loaded:
            if line.startswith('@@'):
                current = Hunk(info=line)
            else:
                current.append(line)
        return diff
        
    
    def __repr__(self):
        return '<Patch: [%s]>' % self.diff
    
    def __eq__(self, other):
        return self._orig == other._orig
    
    def __str__(self):
        #return str(self._orig)
        return self.__json__()
    
    def __json__(self):
        if self._orig is None:
            self._orig = sum([
                [str(hunk.info)] + hunk.lines for hunk in self.diff
            ])
        import simplejson
        return simplejson.dumps(self._orig)
            

    def __add__(self, other):
        new_diffs = []
        my_hunk = 0
        other_hunk = 0
        while True:
            if self.diff[my_hunk].info.start('+') > other.diff[other_hunk].info.start('-'):
                earlier_hunk = self.diff[my_hunk]
                later_hunk = other.diff[other_hunk]
            else:
                earlier_hunk = other.diff[other_hunk]
                later_hunk = my.diff[my_hunk]
            if earlier_hunk.info.end('+') >= later_hunk.info.start('-'):
                new_diffs.append(earlier_hunk)
            else:
                new_lines = earlier_hunk[:earlier_hunk.lines.index(later_hunk[0])]
                new_diffs.append(Hunk(HunkInfo(leftbegin=earlier_hunk.info.start('-'), leftlength=len(new_lines),
                    rightbegin=earlier_hunk.info.start('+'), rightlength=len(new_lines)), new_lines))
            break

    def __neg__(self):
        #this will be efficient until you serialize it.  Then it might be slow.
        return Patch(hunks=[-hunk for hunk in self.diff])
    
    def __sub__(self, other):
        return self + (-other)
    
    def apply(self, init, sign='+'):
        from pprint import pprint
        l = init.split(' ')
        for hunk in self.diff:
            l = hunk.apply(l)
        return ' '.join(l)
    
    def apply_rev(self, init):
        return self.apply(init, '-')            
        
class HunkInfo(object):
    """
    Stores info from "@@ -99,99 +99,99@@" formatted strings
    
    >>> HunkInfo('@@ -1,23 +456,07890 @@')
    <HunkInfo "@@ -1,23 +456,7890 @@">
    >>> HunkInfo(
    ...     leftbegin=1,
    ...     leftlength=23,
    ...     rightbegin=456,
    ...     rightlength=7890,
    ... )
    <HunkInfo "@@ -1,23 +456,7890 @@">
    """
    def __init__(self, *args, **kwargs):
        if len(args) == 1 and isinstance(args[0], str):
            (self.leftbegin,self.leftlength),(self.rightbegin,self.rightlength) = [
                map(int,t.strip('-+').split(',')) for t in args[0].split(' ')[1:-1]
            ]
            return
        if kwargs:
            self.__dict__.update(kwargs)
            return
        raise TypeError("HunkInfo doesn't know what you're talking about.")
    
    def __repr__(self):
        return '<HunkInfo "%s">' % self
    
    def __str__(self):
        return '@@ -%s,%s +%s,%s @@' % (
            self.leftbegin, self.leftlength, self.rightbegin, self.rightlength
        )
    
    def __neg__(self): #for the reverse diff
        return HunkInfo(
            leftbegin =  self.rightbegin,
            leftlength =    self.rightlength,
            rightbegin = self.leftbegin,
            rightlength =   self.leftlength
        )
    
    def start(sign):
        if sign == '+':
            return self.rightbegin - 1
        if sign == '-':
            return self.leftbegin - 1
    
    def end(sign):    
        if sign == '+':
            return self.rightbegin + self.leftlength - 1
        if sign == '-':
            return self.leftbegin + self.rightlength - 1

class Hunk(object):
    """
    Stores one hunk of a diff.
    """
    def __init__(self, info, things=[]):
        self.lines = things
        self.info = HunkInfo(info)
    
    def append(self, thing):
        self.lines.append(thing)
    
    def __getitem__(self, i):
        return lines[i]
    
    def __iter__(self):
        for line in self.lines:
            yield line
    
    def side(self, sign):
        return [
            line[1:] for line in self if line.startswith(' ') or line.startswith(sign)
        ]
    
    def apply(self, l, sign='+'):
        l[self.info.start(sign):self.info.end(sign)] = self.side(sign)
        return l
    
    def __add__(self, other):
        pass
    
    def __neg__(self):
        #reverse info
        info = HunkInfo(
            leftbegin=self.info.rightbegin,
            rightbegin=self.info.leftbegin,
            leftlength=self.info.rightlength,
            rightlength=self.info.leftlength,
        )
        
        #reverse the lines and return
        splutlines = [(l[0],l[1:]) for l in self.lines]
        return Hunk(info, [
            ('+' if sign == '-' else '-' if sign == '+' else ' ') + line
            for sign, line in splutlines
        ])
            
    
orig = """
=== edit Article ===

But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born 

and I will give you a complete account of the system, 

and expound the actual teachings of the great explorer of the truth, 

the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it? 

But who has any right to find fault with a man who chooses to enjoy a pleasure that has no annoying consequences, or one who avoids a pain that produces no resultant pleasure?

"""

edit = orig.replace('the', 'teh')
edit = edit[:150] + edit[200:]

edit2 = edit.replace('pleasure', 'yummy cookies').replace('pain', 'chunky bacon') + '\na few lines at the end'

if __name__ == '__main__':
    import doctest
    doctest.testmod()
